lib/WebSocketClient.ps1


Function Start-TMConsoleWebSocketClient {
    [CmdletBinding()]
    param (
        
        [Parameter(Mandatory = $True)]
        [String]
        $Hostname,
        
        [Parameter(Mandatory = $True)]
        [Int]
        $Port,
        
        [Parameter()]$HostPID = -1,
        
        [Parameter(mandatory = $false)]
        [bool]$OutputVerbose = $false, ## PROD SETTING
        # [bool]$OutputVerbose = $true, ## DEV SETTING

        [Parameter()][Bool]$AllowInsecureSSL = $False,
        
        ## Use if debugging. This bypasses running the ActionRequest in an RS Job, and runs it locally instead.
        [Parameter()][Bool]$AllowDirectExecution = $False

        
    )
    
    begin {
    
        ## Enable Verbose Output if OutputVerbose was true
        if ($OutputVerbose) { 
            $global:VerbosePreference = 'Continue'    
            $VerbosePreference = 'Continue'    
            
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Starting PowerShell Web Socket Client"
            }
        } 

        ##
        ## Create Event Handlers for Runspace Management
        ##
            
        ##
        ## WebSocket Event Hander Definition: Send Data to Web Socket Server
        $EventHandler_WebSocket_SendData = [scriptblock] {
            param($WebSocketClient, $ClientId, $Queues)

            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Starting EventHandler_WebSocket_SendData"
            }

            $CancellationToken = New-Object Threading.CancellationToken($false)
            $WorkItem = $null
            while ($WebSocketClient.State -eq [Net.WebSockets.WebSocketState]::Open) {
                Start-Sleep -Milliseconds 500
                while ($Queues.WebSocketClientSend.TryDequeue([ref] $WorkItem)) {
                    
                    ## Write to Output
                    if ($OutputVerbose) { 
                        Write-Output "EventHandler_WebSocket_SendData Sending: $WorkItem"
                    }

                    [ArraySegment[byte]]$Message = [Text.Encoding]::UTF8.GetBytes($WorkItem)
                    
                    $WebSocketClient.SendAsync(
                        $Message,
                        [System.Net.WebSockets.WebSocketMessageType]::Binary,
                        $true,
                        $CancellationToken
                    ).GetAwaiter().GetResult() | Out-Null
                }
            }

            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending EventHandler_WebSocket_SendData"
            }
        }

        ##
        ## WebSocket Event Hander Definition: Send Data to Web Socket Server
        $EventHandler_WebSocket_ReceiveData = [scriptblock] {
            param($WebSocketClient, $CancellationToken, $ClientId, $Queues)
            
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Verbose "Starting EventHandler_WebSocket_ReceiveData"
            }

            # ## Handle Receiving Data from the WebSocket
            $Size = 32768
            $Array = [byte[]] @(, 0) * $Size
            $ReceiveBuffer = New-Object System.ArraySegment[byte] -ArgumentList @(, $Array)

            While ($WebSocketClient.State -eq 'Open') {
                
                try {
                    $MessageString = ""
                    Do {
                        ## Create an Event to receive the next Buffer
                        $ReceiveDataTask = $WebSocketClient.ReceiveAsync($ReceiveBuffer, $CancellationToken)
                        While (-Not $ReceiveDataTask.IsCompleted) {
                            Start-Sleep -Milliseconds 250    
                        }
                        $ReceiveBuffer.Array[0..($ReceiveDataTask.Result.Count - 1)] | ForEach-Object {
                            $MessageString = $MessageString + [char]$_ 
                        }
                    }
                    Until ($ReceiveDataTask.Result.Count -lt $Size)
                    
                    ## There was a message delivered from TMConsole
                    if ($MessageString) {
                        ## Write to Output
                        if ($OutputVerbose) { 
                            Write-Output "WebSocket Received a Message, Queueing for SessionManager: $MessageString"
                        }
                        
                        ## Queue the Message for the SessionManager to handle
                        $Queues.SessionManager.Enqueue($MessageString)
                    }
                }
                catch {
                    $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                                Type  = 'Debug THROWN ERROR'
                                Error = $_
                            } | ConvertTo-Json -Depth 5))
                    Write-Host $_
                }
            }
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending EventHandler_WebSocket_ReceiveData"
            }
        }

        ##
        ## Runspace Event Handler - Remove Completed Runspace Jobs
        $ScriptBlock_TaskRunspace_Remove_Completed = [scriptblock] {
            param($TMTaskId)
            
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Running ScriptBlock_TaskRunspace_Remove_Completed for $TMTaskId"
            }

            Unregister-Event -SourceIdentifier ($TMTaskId + "_Runspace_StateChanged")
            Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Information")
            Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Progress")
            Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Error")
            Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Verbose")
            Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Debug")
            Unregister-Event -SourceIdentifier ($TMTaskID + "_Stream_Warning")
        
            ## Remove the Runspace Job
            try {
                
                $RSJob = Get-RSJob | Where-Object { $_.Name -like $TMTaskId + '*' }
                Remove-RSJob -Job $RSJob -Force -ErrorAction SilentlyContinue
            }
            catch {
                $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                            Type   = 'SystemError'
                            From   = 'Removing Runspace'
                            Detail = 'RSJob_RemoveCompleted FAILED removing the RSJob' 
                            Error  = $_.Exception.Message
                        } | ConvertTo-Json))
            }
        
            ## Report current status of Runspace Jobs
            $RunningRSJobs = Get-RSJob | Where-Object { $_.State -eq 'Running' }
            $NewStatus = @{
                Type    = 'powershell-server-status'
                Message = @{
                    connectionStatus = 'Connected'
                    serverName       = $Hostname
                    serverStatus     = "Monitoring $($RunningRSJobs.Count) TMC Actions"
                    from             = 'ScriptBlock_TaskRunspace_InvokeActionRequest'
                }
            }
            $Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress))
            
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending ScriptBlock_TaskRunspace_Remove_Completed for $TMTaskId"
            }
        }
    
        ##
        ## Event Hander Definitions: Invoke Action Requests
        $ScriptBlock_TaskRunspace_InvokeActionRequest = [scriptblock] {
            param (
                [Parameter()]
                [PSObject]
                $ActionRequest,

                [Parameter()]
                [System.Boolean]
                $AllowInsecureSSL = $False,
            
                [Parameter()]
                [System.Boolean]
                $AllowDirectExecution = $False,
            
                $Queues,
                $WebSocketClient
            )

            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Starting ScriptBlock_TaskRunspace_InvokeActionRequest"
            }

            ## Ensure all streams are enabled, but Errors stop
            $InformationPreference = 'Continue'
            $VerbosePreference = 'Continue'
            $ProgressPreference = 'Continue'
            $DebugPreference = 'Continue'
            $WarningPreference = 'Continue'
            $ErrorActionPreference = 'Continue'
        
            ## Rename the Variable to make the invocation logic more clear
            $TMTaskID = 'TMTaskId_' + [string]$ActionRequest.task.id
        
            ## Send a set of Startup Messages
            $StatusMessages = @(

                [PSCustomObject]@{
                    TMTaskId = $TMTaskId
                    Type     = 'TaskStarted'
                },
                [PSCustomObject]@{
                    TMTaskId = $TMTaskID
                    Type     = 'Progress'
                    Message  = [PSCustomObject]@{
                        ActivityId        = 0
                        ParentActivityId  = -1
                        Activity          = 'Queued Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title
                        CurrentOperation  = ''
                        StatusDescription = ''
                        PercentComplete   = 0
                        SecondsRemaining  = -1
                        RecordType        = 0
                    }
                }
            )
        
            ## Send the StatusMessages to TMConsole
            $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress))

            # Allow Running Serially for Debugging
            if ($AllowDirectExecution) {
                
                ## Write to Output
                if ($OutputVerbose) { 
                    Write-Output "Starting Scriptblock Direct Execution"
                }

                ## Prepare the Single-Threaded Invocation options
                $InvokeCommandParams = @{
                    ArgumentList = @($ActionRequest, $AllowInsecureSSL)
                    ScriptBlock  = $ActionRequestInnerJobScriptBlock
                }
                try {
                    Invoke-Command @InvokeCommandParams
                }
                catch {
                    throw $_.Exception.Message    
                }
                return
            }
            
            ## Else (Not needed because of the return above)
            ## Prepare the Runspace Job Options
            ## and start the Runspace Job
            try {
                
                ## Write to Output
                if ($OutputVerbose) { 
                    Write-Output "Sending WebSocket Message and Starting Runspace Job"
                }

                ## Send the StatusMessages to TMConsole
                $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress))
                ## Create an appropriate Runspace Name
                $TaskJobName = [String]([string]$TMTaskID + "_" + (Get-Date -Format 'FileDateTimeUniversal'))

                # ## Before Starting the RSJob, Make sure the Provider Module is imported into
                # ## this TMD session so it's loaded and available to supply to any future Provider Tasks
                # ## Include TMD and TM, and add any provider modules
                $ModulesToImport = @('TMConsole.Client', 'TMD.Common', 'TransitionManager')
                $JobParams = @{
                    Name            = $TaskJobName
                    ArgumentList    = @($ActionRequest, $AllowInsecureSSL)
                    ModulesToImport = $ModulesToImport
                    ScriptBlock     = $ActionRequestInnerJobScriptBlock
                }
                
                ## Start the RS Job
                $RSJob = Start-RSJob @JobParams
                
                ## Send a set of Started Up Messages
                $StatusMessages = @(
                    [PSCustomObject]@{
                        TMTaskId = $TMTaskID
                        Type     = 'Progress'
                        Message  = [PSCustomObject]@{
                            ActivityId        = 0
                            ParentActivityId  = -1
                            Activity          = 'Running Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title
                            CurrentOperation  = 'Running'
                            StatusDescription = ''
                            PercentComplete   = 5
                            SecondsRemaining  = -1
                            RecordType        = 0
                        }
                    }
                )
        
                ## Send the StatusMessages to TMConsole
                $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Depth 10 -Compress))
        
                ##
                ## RS Job Output Event Handlers
                ##
        
                ## Create a Common Streams Output Event Splat
                ## Used for Info, Debug, Progress, Error, Verbose and Warning
                $StreamsObjectEventSplat = @{
                    EventName   = 'DataAdded'
                    Action      = $EventHandler_TaskRunspace_Streams
                    MessageData = @{
                        TMTaskId = $TMTaskID
                        Queues   = $Queues
                    }
                }
            
                ## Create an event for Information Stream Output
                $ActivitySplat = @{
                    InputObject      = $RSJob.InnerJob.Streams.Information
                    SourceIdentifier = ($TMTaskID + "_Stream_Information")
                }
                [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat)
        
                ## Create an event for Progress Stream Output
                $ActivitySplat = @{
                    InputObject      = $RSJob.InnerJob.Streams.Progress
                    SourceIdentifier = ($TMTaskID + "_Stream_Progress")
                }
                [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat)
        
                ## Create an event for Progress Error Output
                $ActivitySplat = @{
                    InputObject      = $RSJob.InnerJob.Streams.Error
                    SourceIdentifier = ($TMTaskID + "_Stream_Error")
                }
                [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat)
        
                ## Create an event for Progress Verbose Output
                $ActivitySplat = @{
                    InputObject      = $RSJob.InnerJob.Streams.Verbose
                    SourceIdentifier = ($TMTaskID + "_Stream_Verbose")
                }
                [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat)
        
                ## Create an event for Progress Debug Output
                $ActivitySplat = @{
                    InputObject      = $RSJob.InnerJob.Streams.Debug
                    SourceIdentifier = ($TMTaskID + "_Stream_Debug")
                }
                [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat)
        
                ## Create an event for Progress Warning Output
                $ActivitySplat = @{
                    InputObject      = $RSJob.InnerJob.Streams.Warning
                    SourceIdentifier = ($TMTaskID + "_Stream_Warning")
                }
                [void](Register-ObjectEvent @StreamsObjectEventSplat @ActivitySplat)
        
            
                ## Register Startup and Output events
                $RunspaceStateChangedEventSplat = @{
                    SourceIdentifier = ($TMTaskID + "_Runspace_StateChanged")
                    EventName        = 'InvocationStateChanged'
                    InputObject      = $RSJob.InnerJob
                    Action           = $EventHandler_TaskRunspace_StateChanged
                    MessageData      = @{
                        TMTaskId = $TMTaskID
                        Queues   = $Queues
                    }
                }
                [void](Register-ObjectEvent @RunspaceStateChangedEventSplat)
            
                
                ##
                ## Update the Action Counter in the UI
                ##
                
                ## Get the list of RS Jobs in progress to report Session Manager Status
                $RSJobs = Get-RSJob | Where-Object { $_.State -eq 'Running' }

                ## Update PowershellServerStatus
                $NewStatus = @{
                    Type    = 'powershell-server-status'
                    Message = @{
                        connectionStatus = 'Connected'
                        serverName       = $Hostname
                        serverStatus     = "Monitoring $($RSJobs.Count) TMC Actions"
                        from             = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Started New Action'
                    }
                }
                $Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress))
            }
            catch {
                $NewStatus = @{
                    Type      = 'SystemError'
                    From      = 'Starting ActionRequest RunspaceJob'
                    Message   = 'ScriptBlock_TaskRunspace_InvokeActionRequest - Invocation Error'
                    Exception = $_.Exception.Message
                }
                $Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress))

            }

            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending ScriptBlock_TaskRunspace_InvokeActionRequest"
            }
        }

        ##
        ## Event Hander Definitions: Task Runspace State Change
        $EventHandler_TaskRunspace_StateChanged = [scriptblock] {
            
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Starting EventHandler_TaskRunspace_StateChanged"
            }

            ## Collect the TMTaskId from the MessageData
            $TMTaskId = $Event.MessageData.TMTaskId
            $Queues = $Event.MessageData.Queues

            ## Assign the Stream ID based on a possible redirection
            $StreamId = $Global:TaskStreamRedirections.$TMTaskId ?? $TMTaskId

            ## Process each Event Item
            $Event.SourceArgs | ForEach-Object {
            
                ## Name the variable for convenience
                $NewData = $_
                $NewDataType = $NewData.GetType().ToString()
            
                ## The type of Raised Event determines what to do
                switch ($NewDataType) {
                            
                    ## Handle a PowerShell (session) object
                    'System.Management.Automation.PowerShell' { 
                        
                        ## Get the Job to determine if there's more data
                        $RSJob = Get-RSJob | Where-Object { $_.Name -like $TMTaskId + '*' }

                        ## Write to Output
                        if ($OutputVerbose) { 
                            Write-Output "EventHandler_TaskRunspace_StateChanged -- State Change: $RSJob"
                        }

                        ## Switch on the Invocation State
                        switch ($NewData.InvocationStateInfo.State.ToString()) {
                            'Completed' { 
                            
                                # if ($RSJob.HasMoreData) {
    
                                # $Output = $RSJob | Receive-RSJob
                                # $Output | ConvertTo-Json -Depth 5 -Compress | Write-Host -ForegroundColor green
                                # }

                                # Send a Progress Activity
                                $StatusMessages = @(

                                    [PSCustomObject]@{
                                        TMTaskId = $StreamId
                                        Type     = 'Progress'
                                        Message  = [PSCustomObject]@{
                                            ActivityId        = 0
                                            ParentActivityId  = -1
                                            Activity          = 'Task Completed'
                                            CurrentOperation  = 'Complete'
                                            StatusDescription = ''
                                            PercentComplete   = 100
                                            SecondsRemaining  = -1
                                            RecordType        = 1
                                        }
                                    },
                                    [PSCustomObject]@{
                                        TMTaskId  = $StreamId
                                        Type      = 'TaskCompleted'
                                        RSJobName = $RSJob.Name
                                    }
                                )
                                ## Send the StatusMessages to TMConsole
                                $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Compress))

                                ## SessionManager message
                                $SessionManagerMessage = @{
                                    TMTaskId = $TMTaskId
                                    Type     = "RemoveRunSpace"
                                } | ConvertTo-Json

                                $Queues.SessionManager.Enqueue($SessionManagerMessage)
                            
                            }
                            'Failed' { 
                                
                                ## Capture the error message to send to TMConsole
                                $InvocationError = $NewData.InvocationStateInfo.Reason.ErrorRecord.Exception.Message

                                # Send a Progress Activity
                                $StatusMessages = @(
                                    [PSCustomObject]@{
                                        TMTaskId = $StreamId
                                        Type     = 'Error'
                                        Message  = $InvocationError
                                    },
                                    [PSCustomObject]@{
                                        TMTaskId = $StreamId
                                        Type     = 'Progress'
                                        Message  = [PSCustomObject]@{
                                            ActivityId        = 0
                                            ParentActivityId  = -1
                                            Activity          = 'Task Failed'
                                            CurrentOperation  = 'Failed'
                                            StatusDescription = ''
                                            PercentComplete   = 100
                                            SecondsRemaining  = -1
                                            RecordType        = 2
                                        }
                                    },
                                    [PSCustomObject]@{
                                        TMTaskId = $StreamId
                                        Type     = 'TaskFailed'
                                        Message  = $InvocationError
                                    }
                                )
                                ## Send the StatusMessages to TMConsole
                                $Queues.WebSocketClientSend.Enqueue(($StatusMessages | ConvertTo-Json -Compress))

                                ## SessionManager message
                                $SessionManagerMessage = @{
                                    TMTaskId = $TMTaskId
                                    Type     = "RemoveRunSpace"
                                } | ConvertTo-Json
                                $Queues.SessionManager.Enqueue($SessionManagerMessage)
    
                            }
                            Default {
                                if ($OutputVerbose) {
                                    Write-Output "TMTask: $TMTaskId has some other state: $($NewData.InvocationState.State.ToString())"
                                }
                            }
                        }
                    
                        break
                    }
                
                    # Handle a PSInvocationStateChangedEventArgs
                    'System.Management.Automation.PSInvocationStateChangedEventArgs' { 
                    
                        ## This State Changed Object is Redundent. The 'PowerShell' object that is also passed
                        ## Contains all of the information needed and this Event Args can safely be ignored
                        break
                    }
                
                    ## Handle anything that wasn't a known type
                    Default {
                        Write-Host "Received an unhandled Object!! $($NewData.GetType().ToString())" -ForegroundColor Red
                        Write-Host "`t$($NewData | ConvertTo-Json -EnumsAsStrings -Depth 3)" -ForegroundColor Red
                    }
                }
            }
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending EventHandler_TaskRunspace_StateChanged"
            }
        }

        ##
        ## Event Hander Definitions: StreamOutput from Task Runspaces
        $EventHandler_TaskRunspace_Streams = [scriptblock] {
        
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Starting EventHandler_TaskRunspace_Streams"
            }
            
            ## Collect the TMTaskId from the MessageData
            $TMTaskId = $Event.MessageData.TMTaskId
            $Queues = $Event.MessageData.Queues
            
            ## Assign the Stream ID based on a possible redirection
            $StreamId = $Global:TaskStreamRedirections.$TMTaskId ?? $TMTaskId
            
            ## Create a Messages Arrays to store the incoming messages in
            $MessagesToProcess = [System.Collections.ArrayList]::new()
            $MessagesToSend = [System.Collections.ArrayList]::new()
            
            ##
            ## Iterate to collect any SourceArgs items and move them to a Processing array
            ## This is done in this fashion to quickly collect the messages for later processing
            ## They are not processed and sent initally, because this function must also clear the stream
            ## If this is done too long after the messages come in, you risk clearing unprocessed items.
            ##

            ## Save each of the Messages delivered to a separate Array
            foreach ($NewData in $Event.SourceArgs[0]) {
                
                ## Move the NewData item into the Processing Queue Array
                [void]$MessagesToProcess.Add($NewData)
            }
               
            ## With the SourceArgs messages safely stored,
            ## Clear the Event Stream
            $Event.Sender.clear() 

            ##
            ## Process each message, collecting Tokenized messages to send.
            ## Sending is not done one-at-a-time, but as an array so Angular
            ## Has the ability to process multiple messages before publishing an
            ## Observable update

            ## Process each Message waiting to be processed
            foreach ($NewData in $MessagesToProcess) {

                ## Switch based on the type of object in the stream
                switch ($NewData.GetType().ToString()) {
                    
                    ## Error Records
                    'System.Management.Automation.ErrorRecord' { 
                        
                        ## Do nothing here because the Runspace Monitoring will pick up the error and supply
                        ## it to TMConsole to be handled.
                        break
                    }
                
                    ## Write-Progress Messages
                    'System.Management.Automation.ProgressRecord' { 
                        
                        ## Ignore ActivityID -1 (Used by Invoke-WebRequest and others for temporary Progress Bars)
                        if (`
                            ($NewData.Activity -ne 'Reading web response')`
                                -and ($NewData.ActivityId -ge 0) `
                                -and ($NewData.PercentComplete -ge 0)`
                        ) {

                            # Setup a Progress Message to send
                            [void]$MessagesToSend.Add([PSCustomObject]@{
                                    TMTaskId = $StreamId
                                    Type     = 'Progress'
                                    Message  = @{
                                        Activity          = $NewData.Activity
                                        ActivityId        = $NewData.ActivityID
                                        ParentActivityId  = $NewData.ParentActivityId
                                        CurrentOperation  = $NewData.CurrentOperation
                                        StatusDescription = $NewData.StatusDescription
                                        SecondsRemaining  = $NewData.SecondsRemaining
                                        PercentComplete   = $NewData.PercentComplete
                                        RecordType        = $NewData.RecordType
                                    }
                                }
                            )
                        }
                        break
                    }
                
                    ## Write-Verbose Messages
                    'System.Management.Automation.VerboseRecord' { 
                        [void]$MessagesToSend.Add([PSCustomObject]@{
                                TMTaskId = $StreamId
                                Type     = 'Verbose'
                                Message  = $NewData.Message
                            }
                        )
                        break
                    }
                    
                    ## Write-Warning Records
                    'System.Management.Automation.WarningRecord' { 
                        [void]$MessagesToSend.Add([PSCustomObject]@{
                                TMTaskId = $StreamId
                                Type     = 'Warning'
                                Message  = $NewData.Message
                            }
                        )
                        break
                    }
                    
                    ## Write-Debug Records
                    'System.Management.Automation.DebugRecord' { 
                        [void]$MessagesToSend.Add([PSCustomObject]@{
                                TMTaskId = $StreamId
                                Type     = 'Debug'
                                Message  = $NewData.Message
                            }
                        )
                        break
                    }
                    
                    <##
                        Write-Host, Out-Host Records
                        $NewData.MessageData is like
                        @{
                            Message = 'Hello, World!'
                            ForegroundColor = 'White'
                            BackgroundColor = 'Black'
                            NoNewLine = $True|$False
                        }
                        for standard 'Write-Host' output.
 
 
                        Within the TMConsole.Client UI command set, there are other types of output that are plucked from this stream
                    #>

                    'System.Management.Automation.InformationRecord' { 
                    
                        ## TMConsole.Client commands may prefix output with code "||TMC:" to perform an alternate action
                        ## These Write-Host output objects are a token with a type that is handled specifically by the TMConsole UI
                        ## to display a beautiful component or to invoke some functionality within TMConsole.
                        if (($NewData.MessageData.Message.Length -gt 5) -and ($NewData.MessageData.Message.substring(0, 6) -eq '||TMC:')) {
                            
                            # ## Trim the leading characters
                            # $Message = $NewData.MessageData.Message -replace "\|\|TMC:", ''
                            
                            ## Convert the reaminder of the first line from JSON
                            $TmcObject = ($Message -split "`n")[0] | ConvertFrom-Json
                            $Queues.WebSocketClientSend.Enqueue(($DebugStatus | ConvertTo-Json -Compress))
                            
                            ## Handle Different Types of TMCObjects
                            switch ($TmcObject.Type) {
                                
                                ## Banners are created by Write-Banner from TMConsole.Client
                                ## They will be displayed with a CSS styled banner componenet
                                'Banner' {
                                    $Queues.WebSocketClientSend.Enqueue(
                                            ([PSCustomObject]@{
                                            TMTaskId = $StreamId
                                            Type     = 'Banner'
                                            Message  = $TmcObject
                                        } | ConvertTo-Json -Compress)
                                    )
                                }

                                <#
                                    BrokerUpdate messages are created when a Broker starts a Subject Task and when a Subject ends.
                                     
                                    BrokerUpdate = @{
                                        Type = 'BrokerUpdate'
                                        TMTaskId = TMTaskId_{actionrequest.task.id}
                                        TargetStreamId = TMTaskId_{subjecttask.task.id}
                                    }
                                     
                                    The purpose of this message is 2 fold:
                                        1 - to record a change to the target Task ID stream that should receive the console output
                                            When a broker first starts, the Stream ID is that of the Broker. This means console/progress output
                                            emitted by the Broker is displayed in the Progress/Console _for the broker_ task.
 
                                            When a BrokerUpdate provides an alternate StreamID, that stream then becomes the target
                                            Task ID. Any output received by the SessionManager will be streamed to the Subject Task ID.
 
                                            A BrokerUpdate is also received to return the Broker to 'normal', by supplying the TargetStreamId
                                            of the Broker Task. This restores the output stream to the Broker task, not the subject.
 
                                        2 - When a BrokerUpdate has differing StreamIds (meaning it's output is redirected to a subject),
                                            this also indicates that the Subject Task should be displayed in TMConsole's Task List.
                                            When this occurs, the BrokerUpdate is forwarded to Angular, so it can caretake for ensuring
                                            that the Subject Task is then brought into the TaskList view.
 
 
                                #>

                                'BrokerUpdate' {
                                    
                                    ## Calculate the TMTaskId_ for the Target Stream
                                    $SubjectId = 'TMTaskId_' + $TmcObject.SubjectTaskId

                                    ## If the BrokerStarting a Redirect
                                    if ($TmcObject.Change -eq 'StartRedirect') {
                                        $Global:TaskStreamRedirections.$TMTaskId = $SubjectId
                                    }
                                    
                                    ## The Broker is ending a Stream Redirection
                                    else {
                                        
                                        ## Get an existing Stream Redirection Record for the Broker task $TMTaskId
                                        if ($Global:TaskStreamRedirections.Keys -contains $TMTaskId) {
                                            [void]$Global:TaskStreamRedirections.Remove($TMTaskId)
                                        }
                                    }

                                    ## Create a message to TMC so the UI can add the Subject Task
                                    [void]$MessagesToSend.Add($TmcObject)
                                }
                            }
                        }
                        
                        ## Plain Write-host InformationRecord objects
                        Else {
                            
                            [void]$MessagesToSend.Add([PSCustomObject]@{
                                    TMTaskId = $StreamId
                                    Type     = 'Information'
                                    Message  = $NewData.MessageData
                                }
                            )
                        }   
                       
                        break
                    }
                
                    ## Error Records
                    Default {
                        if ($OutputVerbose) {
                            Write-Output ('Unknown Data: ' + ($NewData | ConvertTo-Json))
                        }
                    }
                }
            }
            
            ##
            ## Send any messages collected as one array object.
            ##

            # If there were any messages collected to send
            if ($MessagesToSend.Count -gt 0) {
                   
                ## Convert the data to JSON
                $MessagesJSON = ($MessagesToSend | ConvertTo-Json -Depth 10 -Compress )
                    
                ## Send the Update to TMConsole
                $Queues.WebSocketClientSend.Enqueue($MessagesJSON)
            }

            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending EventHandler_TaskRunspace_Streams"
            }
        }
        
        ##
        ## Define ActionRequest-Task Executing Scriptblock that runs inside the RSJob
        ##
        
        ## Build up the proper runspace invocation that isn't otherwise working
        $ActionRequestInnerJobScriptBlock = [scriptblock] {
            param($ActionRequest, $AllowInsecureSSL)
            
            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Starting ActionRequestInnerJobScriptBlock"
            }

            ## Ensure all streams are enabled
            $InformationPreference = 'Continue'
            $VerbosePreference = 'Continue'
            $ProgressPreference = 'Continue'
            $DebugPreference = 'Continue'
            $WarningPreference = 'Continue'
            $ErrorActionPreference = 'Continue'

            ## Sleep long enough to let the Event Handler attach
            Start-Sleep -Milliseconds 200
        
            ## Write a Verbose Message starting the task
            Write-Verbose ('Starting Task TMTaskId_' + [string]$ActionRequest.task.taskNumber + ': ' + $ActionRequest.task.title)

            # ## Import the TMC Action Request, which also loads Provider Modules
            . Import-TMCActionRequest -PSObjectActionRequest $ActionRequest

            # ## Create a Parameters Variable from the Action Script
            New-Variable -Name Params -Scope Global -Value $ActionRequest.params -Force
        
            ## Add $Credential if there is one
            if ($ActionRequest.PSCredential) {
                New-Variable -Scope Global -Name Credential -Value $ActionRequest.PSCredential -Force
            }

            ## Invoke the User Script block
            $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script)
        
            ## Run the User Provided Script
            try {

                Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope
            }
            catch {
                throw $_
            }

            ## Create a Data Options parameter for the Complete-TMTask command
            $CompleteTaskParameters = @{}

            ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion
            if ($Global:TMAssetUpdates) {
                $CompleteTaskParameters = @{ 
                    Data = @{
                        assetUpdates = $Global:TMAssetUpdates
                    }
                }
            }

            ## Add SSL Exception if necessary
            if ($AllowInsecureSSL) {
                $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $True
            }
     
            ## Complete the TM Task, sending Updated Data values for the task Asset
            if ($ActionRequest.HostPID -ne -1) {
                Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters
            }

            ## Write to Output
            if ($OutputVerbose) { 
                Write-Output "Ending ActionRequestInnerJobScriptBlock"
            }
        }
    }

    ## Execute the Main Function of the Script
    Process {
        Try {
            ## Create the WebSocket Send Queue as a Synchronized Queue so Event Handlers can access it
            New-Variable -Name Queues -Force -Scope Global -Value @{
                WebSocketClientSend    = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]'
                WebSocketClientReceive = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]'
                SessionManager         = New-Object 'System.Collections.Concurrent.ConcurrentQueue[String]'
            }
            
            # ## Queue the first message up
            # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
            # Type = 'Debug'
            # Detail = 'SessionManager is starting Process'
            # } | ConvertTo-Json))

            Do {
                
                ## Create a Stream Redirection Cache
                New-Variable -Name TaskStreamRedirections -Force -Scope Global -Value @{
                    Robin = 'Streb'
                    Pup   = 'Atticus'
                }
                
                # Store the Configuration Details for this Remote Session (Web Socket Server Host)
                New-Variable -Name PowershellServerStatus -Force -Scope Global -Value @{
                    connectionStatus = 'Connecting'
                    serverName       = $Hostname
                    serverStatus     = 'Connecting'
                }

                # ## Make the Inital WebSocket and Connect
                # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                # Type = 'Debug'
                # Detail = 'SessionManager Connecting to TMConsole WebSocket'
                # } | ConvertTo-Json))
                
                ## Create Send and Receive queues for the Web Socket
                # [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
                $ClientId = New-Guid | Select-Object -ExpandProperty Guid
                $WebSocketClient = New-Object System.Net.WebSockets.ClientWebSocket                                             
                $CancellationToken = New-Object System.Threading.CancellationToken                                                   
      
                # $WebSocketClient.Options.RemoteCertificateValidationCallback = { return $true }

                # [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
                # $handler = [System.Net.Http.HttpClientHandler]::new()
                # $ignoreCerts = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator
                # $handler.ServerCertificateCustomValidationCallback = $ignoreCerts
                ## Establish the connection and wait until it answers
                $VerbosePreference = 'Continue'
                $Global:VerbosePreference = 'Continue'
               
                ## Establish the WebSocket connection
                $WssEndpoint = "wss://$($Hostname):$($Port)"
                Write-Verbose "Connecting to WSS Endpoint: $WssEndpoint"

                $Connection = $WebSocketClient.ConnectAsync($WssEndpoint, $CancellationToken)                                                  
                While (!$Connection.IsCompleted) { Start-Sleep -Milliseconds 100 }
                if ($Connection.IsFaulted) {
                    throw "Connection Faulted: $($Connection.Exception.InnerException.InnerException.InnerException.Message)"
                }

                ## Update PowershellServerStatus
                $NewStatus = @{
                    Type    = 'powershell-server-status'
                    Message = @{
                        connectionStatus = 'Connected'
                        serverName       = $Hostname
                        serverStatus     = "Monitoring 0 TMC Actions"
                        from             = 'WebSocket Client Connected'
                    }
                }
                $global:Queues.WebSocketClientSend.Enqueue(($NewStatus | ConvertTo-Json -Compress))

                ##
                ## Start An Event Handler for the WebSocketClientSend Queue
                ## This caretakes to empty the 'SendQueue' that the Runspaces have access to
                ##
                
                # ## Start a WebSocket Sending Runspace
                # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                # Type = 'Debug'
                # Detail = 'SessionManager is Creating a WebSocket Sending Runspace'
                # } | ConvertTo-Json))

                $SendRunspace = [PowerShell]::Create()
                $SendRunspace.AddScript($EventHandler_WebSocket_SendData).
                AddParameter("WebSocketClient", $WebSocketClient).
                AddParameter("ClientId", $ClientId).
                AddParameter("Queues", $global:Queues).BeginInvoke() | Out-Null
                
                # ## Start a WebSocket Receiving Runspace
                # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                # Type = 'Debug'
                # Detail = 'SessionManager is Creating a WebSocket Receiving Runspace'
                # } | ConvertTo-Json))

                $ReceiveRunspace = [PowerShell]::Create()
                $ReceiveRunspace.AddScript($EventHandler_WebSocket_ReceiveData).
                AddParameter("WebSocketClient", $WebSocketClient).
                AddParameter("CancellationToken", $CancellationToken).
                AddParameter("ClientId", $ClientId).
                AddParameter("Queues", $global:Queues).BeginInvoke() | Out-Null
                
                # ## Start a WebSocket Receiving Runspace
                # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                # Type = 'Debug'
                # Detail = 'SessionManager is Now waiting for for Instructions'
                # } | ConvertTo-Json))

                ## Run a While Connection=Open loop
                While ($WebSocketClient.State -eq 'Open') {
                    
                    ## Sleep
                    Start-Sleep -Milliseconds 250

                    $MessageString = ""
                    if ($global:Queues.SessionManager.TryDequeue([ref]$MessageString)) {
        
                        ## Convert the incoming String data to an Object
                        $Message = $MessageString | ConvertFrom-Json

                        $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                                    Type        = 'Debug'
                                    Detail      = 'SessionManager received a message'
                                    MessageType = $Message.Type
                                } | ConvertTo-Json))

                        ## Switch Activity based on the Type in the Message
                        $InvokeSplat = @{}
                        switch ($Message.Type) {
                            'ActionRequest' {  
        
                                $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                                            TMTaskId = 'TMTaskId_' + $Message.task.id
                                            Type     = 'Debug'
                                            Detail   = 'Invoking ActionRequest'
                                        } | ConvertTo-Json))

                                ## Run the Action Request
                                $InvokeSplat = @{
                                    ScriptBlock  = $ScriptBlock_TaskRunspace_InvokeActionRequest
                                    ArgumentList = $Message, $AllowInsecureSSL, $AllowDirectExecution, $global:Queues, $WebSocketClient
                                }
                                try {

                                    Invoke-Command @InvokeSplat
                                }
                                catch {
                                    $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                                                Type         = 'SystemError'
                                                From         = 'SessionManager Invoking ActionRequest'
                                                ErrorMessage = $_.Exception.Message
                                                StackTrace   = $_.Exception.StackTrace
                                                ScriptName   = $_.InvocationInfo.ScriptName
                                                ErrorLine    = $_.InvocationInfo.ScriptLineNumber
                
                                            } | ConvertTo-Json -Depth 3))
                                }
                            }

                            'RemoveRunspace' {  
        
                                # $global:Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                                # TMTaskId = $Message.TMTaskId
                                # Type = 'Debug'
                                # Detail = 'Removing Runspace'
                                # } | ConvertTo-Json))

                                ## Run the Action Request
                                $InvokeSplat = @{
                                    ScriptBlock  = $ScriptBlock_TaskRunspace_Remove_Completed
                                    ArgumentList = $Message.TMTaskId
                                }
                                try {

                                    Invoke-Command @InvokeSplat
                                }
                                catch {
                                    $Queues.WebSocketClientSend.Enqueue(([PSCustomObject]@{
                                                Type         = 'SystemError'
                                                From         = 'SessionManager Invoking RemoveRunspace'
                                                ErrorMessage = $_.Exception.Message
                                                StackTrace   = $_.Exception.StackTrace
                                                ScriptName   = $_.InvocationInfo.ScriptName
                                                ErrorLine    = $_.InvocationInfo.ScriptLineNumber
                
                                            } | ConvertTo-Json -Depth 3))
                                }
                            }
                            Default {}
                        }
                    }
                }
                Write-Verbose "WebSocketConnection is no longer open!"
                
            } Until (!$Connection)
            Write-Verbose "WebSocketConnection is now closed"
        }
        Catch {
            throw $_
        }
    }

    End {
        Write-Verbose "TMConsole.WebSocketClient is ending"
        If ($WebSocketClient) { 
            Write-Verbose "Closing websocket"
            $WebSocketClient.Dispose()
        }
        Write-Verbose "TMConsole.WebSocketClient has ended"
    }
}